Skip to main content

Stack vs Heap

StackHeap
FastSlower (but flexible)
Fixed sizeDynamic size
LIFO (last in, first out)Arbitrary order
Known at compile timeKnown at runtime
Automatically managedExplicitly managed (by Rust rules)

The Stack

The stack is a region of memory that:

  • Grows and shrinks automatically
  • Stores values with a known, fixed size
  • Is extremely fast to access
fn main() {
let x: i32 = 5;
let y: i32 = x;
}

What happens in memory

  • x is pushed onto the stack
  • y gets a copy of the value
  • Both values exist independently
  • No ownership complexity

Why copying is allowed:

  • i32 has a fixed size
  • No heap allocation
  • Implements Copy

Stack Frames

Each function call gets its own stack frame:

fn foo() {
let a = 10;
}

fn main() {
foo();
}
  • a exists only inside foo’s stack frame
  • When foo returns, the frame is popped
  • Memory is instantly reclaimed

This is why stack memory is so fast.

The Heap

The heap is used when:

  • Size is unknown at compile time
  • Data needs to live longer than a stack frame
  • Data needs to be shared or grow dynamically

Heap allocation is:

  • Slower
  • More flexible
  • Requires explicit rules to manage safely

Example: Heap Allocation with String

let s = String::from("hello");

Memory layout

Stack:           Heap:
┌─────────────┐ ┌─────────────┐
│ s │→ │ "hello" │
│ ptr │ │ │
│ len │ │ │
│ capacity │ └─────────────┘
└─────────────┘
  • The String struct lives on the stack
  • The actual text lives on the heap
  • s owns the heap allocation

Why Ownership Matters for the Heap

Problem Without Ownership

In C/C++:

char* s = malloc(5);
free(s);
printf("%s", s); // 💥 undefined behavior

Use-after-free.

Rust makes this impossible.

Move Semantics (Heap Safety)

let s1 = String::from("hello");
let s2 = s1;
  • Stack data (ptr, len, capacity) is moved
  • Heap allocation is NOT copied
  • s1 is invalidated
  • Only one owner remains

This prevents double free.

Stack vs Heap in Function Calls

Stack-only argument

fn takes_i32(x: i32) {}

let a = 5;
takes_i32(a); // copy
  • Cheap
  • No ownership transfer

Heap-allocated argument

fn takes_string(s: String) {}

let s = String::from("hello");
takes_string(s); // move
  • Ownership moves into the function
  • Heap memory freed when function ends

Borrowing: Safe Heap Access

fn print(s: &String) {
println!("{}", s);
}

let s = String::from("hello");
print(&s);
  • Stack reference passed
  • Heap data is not moved
  • Owner remains responsible for freeing

Borrowing exists because heap data is expensive and dangerous to duplicate.

Mutable Borrowing & Heap Writes

fn append(s: &mut String) {
s.push_str(" world");
}

let mut s = String::from("hello");
append(&mut s);

Why only one mutable borrow?

  • Multiple writers to heap memory = data races
  • Rust forbids this at compile time

Stack Data Referencing Heap Data

let s = String::from("hello");
let r = &s;
  • r is on the stack
  • r points to stack data (s)
  • s points to heap data

Rust ensures:

  • r cannot outlive s
  • s frees heap memory exactly once

Example: Dangling Pointer (Prevented)

let r: &String;

{
let s = String::from("hello");
r = &s; // ❌ compile-time error
}

Why?

  • s’s stack frame disappears
  • Heap memory would be freed
  • r would dangle

Rust refuses to compile.

Stack vs Heap and Lifetimes

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
  • Lifetimes track stack references
  • Ownership controls heap memory
  • Together, they prevent dangling pointers

Performance Perspective

AspectStackHeap
AllocationFreeExpensive
DeallocationFreeExpensive
Cache-friendlyYesLess
Ownership neededNoYes

Rust:

  • Prefers stack allocation by default
  • Uses heap only when necessary
  • Makes heap usage explicit

Common Heap Types in Rust

  • String
  • Vec<T>
  • Box<T>
  • Rc<T> / Arc<T>

All of them:

  • Own heap data
  • Use ownership rules to free memory safely

Mental Model (This One Sticks)

  • Stack = short-lived, fixed-size, automatic
  • Heap = long-lived, dynamic, owned
  • Ownership = who frees the heap
  • Borrowing = temporary access
  • Lifetimes = how long references are valid

Why Rust Cares So Much

Because almost all memory bugs come from:

  • Heap misuse
  • Aliasing + mutation
  • Lifetime mismatches

Rust solves these without a garbage collector.